/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */

#include "Channel.h"
#include "Hardware.h"
#include <logs/Log.h>
#include <math/utl.h>
#include <noise/Manager.h>
#include <noise/hard/Hardware.h>
#include <Ks.h>
#include <KsMedia.h>

#include <boost/bind.hpp>

static HRESULT hr;

namespace liba {
namespace noise {
namespace hard {
namespace directx8 {

static double calculate_attenuation(double distance, double min_distance, double max_distance)
{
	distance = math::clamp( distance, min_distance, max_distance );
	return pow( 2.0, -distance/min_distance);
}

static const int frequency_2d = 22050;
static const int frequency_3d = 22050;
static const int frequency_surround = 22050;
static const double minimum_update_part = 0.25;

ChannelStaticRep::ChannelStaticRep(const ChannelStaticRepName & name):
	buffer( 0 ),
	size( 0 )
{
	source.reset( Manager::create_datasource( name.name ));
	if( !source.get() )
		throw GluckBadResource( __FUNCTION__, "Could not create DataSource for static channel", name.name );

	DSBUFFERDESC desc;
	memset( &desc, 0, sizeof(DSBUFFERDESC) );
    desc.dwSize = sizeof(DSBUFFERDESC); 

	memset( &wfx, 0, sizeof(WAVEFORMATEXTENSIBLE) );

	if( name.channel_type == hard::Hardware::channel_type_2d )
	{
		wfx.Format.wFormatTag = WAVE_FORMAT_PCM;
		wfx.Format.nChannels = 2;
//		wfx.Format.nSamplesPerSec = frequency_2d;
		wfx.Format.nSamplesPerSec = source->get_frequency();
		wfx.Format.wBitsPerSample = 16;
		wfx.Format.nBlockAlign = wfx.Format.wBitsPerSample / 8 * wfx.Format.nChannels;
		wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign;

		desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 |   // Always a good idea
			DSBCAPS_GLOBALFOCUS |
			DSBCAPS_CTRLPAN |
			DSBCAPS_CTRLFREQUENCY |
			DSBCAPS_CTRLVOLUME;
	}
	if( name.channel_type == hard::Hardware::channel_type_3d )
	{
		wfx.Format.wFormatTag = WAVE_FORMAT_PCM;
		wfx.Format.nChannels = 1;
//		wfx.Format.nSamplesPerSec = frequency_3d;
		wfx.Format.nSamplesPerSec = source->get_frequency();
		wfx.Format.wBitsPerSample = 16;
		wfx.Format.nBlockAlign = wfx.Format.wBitsPerSample / 8 * wfx.Format.nChannels;
		wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign;

		desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 |   // Always a good idea
			DSBCAPS_GLOBALFOCUS |
			DSBCAPS_CTRL3D |
			DSBCAPS_CTRLVOLUME;
	}
	if( name.channel_type == hard::Hardware::channel_type_surround )
	{
		wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
		wfx.Format.nChannels = 6;
//		wfx.Format.nSamplesPerSec = frequency_surround;
		wfx.Format.nSamplesPerSec = source->get_frequency();
		wfx.Format.wBitsPerSample = 16;
		wfx.Format.cbSize = 22;
		wfx.Format.nBlockAlign = wfx.Format.nChannels * wfx.Format.wBitsPerSample / 8;
		wfx.Format.nAvgBytesPerSec = wfx.Format.nBlockAlign * wfx.Format.nSamplesPerSec;
		wfx.Samples.wValidBitsPerSample = 16;
		wfx.dwChannelMask =  SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT |
							SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY |
							SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT;
		wfx.SubFormat =  KSDATAFORMAT_SUBTYPE_PCM;  // Specify PCM

		desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 |   // Always a good idea
			DSBCAPS_GLOBALFOCUS |
			DSBCAPS_CTRLVOLUME;
	}
	desc.dwFlags |= DSBCAPS_LOCDEFER;

	desc.lpwfxFormat = &wfx.Format;
	size = desc.dwBufferBytes = wfx.Format.nBlockAlign * source->get_length();
	hr = name.hardware->get_device()->CreateSoundBuffer( &desc, &buffer, 0 );
	if( !buffer )
	{
		logs::log() << logs::error << "CreateSoundBuffer() failed. sound: " << name.name << "\n";
		return; // very bad...
	}

	void * ptr1, * ptr2;
	DWORD bytes1, bytes2;
	hr = buffer->Lock( 0, size,
				  &ptr1, &bytes1, &ptr2, &bytes2,
				  0 );
	if( FAILED( hr ) )
	{
		logs::log() << logs::error << "buffer->Lock() failed. sound: " << name.name << "\n";
		return; // very bad...
	}
	char * dst_ptr = reinterpret_cast<char *>( ptr1 );
	int required_samples = bytes1 / wfx.Format.nBlockAlign;
	while( required_samples )
	{
		int decoded_samples = source->decode( source->get_length() );
		if( decoded_samples == 0 )
		{
			logs::log() << logs::error << "SoundSource failed to decode promissed number of samples: " << name.name << "\n";
			break; // very bad
		}
		required_samples -= decoded_samples;

		if( name.channel_type == hard::Hardware::channel_type_2d )
		{
			source->get_data( DataSource::STEREO_LEFT, dst_ptr, wfx.Format.nBlockAlign, 16 );
			source->get_data( DataSource::STEREO_RIGHT, dst_ptr + 2, wfx.Format.nBlockAlign, 16 );
		}
		if( name.channel_type == hard::Hardware::channel_type_3d )
		{
			source->get_data( DataSource::MONO, dst_ptr, wfx.Format.nBlockAlign, 16 );
		}
		if( name.channel_type == hard::Hardware::channel_type_surround )
		{
			source->get_data( DataSource::SURROUND_FRONTLEFT, dst_ptr, wfx.Format.nBlockAlign, 16 );
			source->get_data( DataSource::SURROUND_FRONTRIGHT, dst_ptr + 2, wfx.Format.nBlockAlign, 16 );
			source->get_data( DataSource::SURROUND_CENTER, dst_ptr + 4, wfx.Format.nBlockAlign, 16 );
			source->get_data( DataSource::SURROUND_SUBWOOFER, dst_ptr + 6, wfx.Format.nBlockAlign, 16 );
			source->get_data( DataSource::SURROUND_REARLEFT, dst_ptr + 8, wfx.Format.nBlockAlign, 16 );
			source->get_data( DataSource::SURROUND_REARRIGHT, dst_ptr + 10, wfx.Format.nBlockAlign, 16 );
		}
		dst_ptr += decoded_samples * wfx.Format.nBlockAlign;
	}
	buffer->Unlock( ptr1, bytes1, ptr2, 0 ); // We tell driver that we have not written any data to second location
}

ChannelStaticRep::~ChannelStaticRep()
{
	if( buffer )
	{
		buffer->Release();
		buffer = 0;
	}
}


IDirectSoundBuffer * ChannelStaticRep::create_diplicate(const ChannelStaticRepName & name)const
{
	IDirectSoundBuffer * result;
	name.hardware->get_device()->DuplicateSoundBuffer( buffer, &result );
	if( !result )
		throw Gluck(__FUNCTION__, "DuplicateSoundBuffer() failed" );
	return result;
}

Channel::Channel()
	: buffer(0)
	, buffer3d(0)
	, exact_volume(1.0)
	, exact_pan(0)
	, min_distance(1)
	, max_distance(1E6)
	, loop(true)
{
	this->RegisterProperty( loop_name, &this->loop);
	this->RegisterProperty( volume_name, &this->exact_volume);
	this->RegisterProperty( minimum_distance_name, &this->min_distance);
	this->RegisterProperty( maximum_distance_name, &this->max_distance);
	this->RegisterProperty( pan_name, &this->exact_pan);
	this->RegisterProperty( position_name, &this->exact_position);
	this->RegisterProperty( speed_name, &this->exact_speed);

	exact_volume.Changed().connect( boost::bind( &Channel::SetVolume, this, _1 ) );
	min_distance.Changed().connect( boost::bind( &Channel::SetMinDistance, this, _1 ) );
	max_distance.Changed().connect( boost::bind( &Channel::SetMaxDistance, this, _1 ) );
	exact_pan.Changed().connect( boost::bind( &Channel::SetPan, this, _1 ) );
	exact_position.Changed().connect( boost::bind( &Channel::SetPosition, this, _1 ) );
	exact_speed.Changed().connect( boost::bind( &Channel::SetSpeed, this, _1 ) );
}

Channel::~Channel()
{}

void Channel::SetPosition(const math::Vector3<float> &value)
{
	if( buffer3d )
		buffer3d->SetPosition( value[0], value[1], value[2], DS3D_DEFERRED );
}

void Channel::SetSpeed(const math::Vector3<float> &value)
{
	if( buffer3d )
		buffer3d->SetVelocity( value[0], value[1], value[2], DS3D_DEFERRED );
}

void Channel::SetVolume(double value)
{
	if( buffer )
		buffer->SetVolume( Hardware::convert_linear_volume( exact_volume ) );
}

void Channel::SetPan(double value)
{
	int pan = int(DSBPAN_LEFT + (DSBPAN_RIGHT - DSBPAN_LEFT) * (value + 1)/2);
	if( buffer )
		buffer->SetPan( pan );
}

void Channel::SetMinDistance(double value)
{
	if( buffer3d )
		buffer3d->SetMinDistance( (D3DVALUE)value, DS3D_DEFERRED );
}

void Channel::SetMaxDistance(double value)
{
	if( buffer3d )
		buffer3d->SetMaxDistance( (D3DVALUE)value, DS3D_DEFERRED );
}

ChannelStatic::ChannelStatic(Hardware * ha, const Atom & channel_type, const filesystem::String & name):
	finished( true ),
	approx_vol( 0 ),
	position( 0 )
{
	load_rep(ChannelStaticRepName(ha, channel_type, name));
	ha->add_static_channel(this);
}

ChannelStatic::~ChannelStatic()
{
	if( get_rep() )
	{
		stop();
		get_rep_name().hardware->remove_static_channel(this);
	}
}


void ChannelStatic::life_cycle(double delta_time, const math::Vector3<float> & listener_position)
{
	if( !get_rep() )
		return;
	approx_vol = calculate_attenuation( exact_position.Get().length(listener_position), min_distance, max_distance );
	if( !buffer && !finished )
	{
		position += int(get_rep()->get_frequency() * delta_time) * get_rep()->get_block_align();
		if( position > (unsigned)get_rep()->get_size() )
			if( loop )
				position %= get_rep()->get_size();
			else
				finished = true;
	}
	if( buffer )
	{
		DWORD status = 0;
		buffer->GetStatus(&status);
		if( !(status & DSBSTATUS_PLAYING) )
		{
			begone();
			finished = true;
		}
	}
}

void ChannelStatic::come_to_existance()
{
	if( !get_rep() )
		return;
	buffer = get_rep()->create_diplicate( get_rep_name() );
	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		hr = buffer->QueryInterface( IID_IDirectSound3DBuffer, (void**)&buffer3d );
		if( buffer3d )
		{
			buffer3d->SetPosition( exact_position.Get()[0], exact_position.Get()[1], exact_position.Get()[2], DS3D_DEFERRED );
			buffer3d->SetVelocity( exact_speed.Get()[0], exact_speed.Get()[1], exact_speed.Get()[2], DS3D_DEFERRED );
			buffer3d->SetMinDistance( (D3DVALUE)min_distance, DS3D_DEFERRED );
			buffer3d->SetMaxDistance( (D3DVALUE)max_distance, DS3D_DEFERRED );
		}
	}
	buffer->SetVolume( Hardware::convert_linear_volume( exact_volume ) );
	int pan = int(DSBPAN_LEFT + (DSBPAN_RIGHT - DSBPAN_LEFT) * (exact_pan + 1)/2);
	buffer->SetPan( pan );

	buffer->SetCurrentPosition( position );
	buffer->Play(0, 0, (loop ? DSBPLAY_LOOPING : 0) );
}

void ChannelStatic::begone()
{
	DWORD write_cursor = 0;
	buffer->GetCurrentPosition( &position, &write_cursor );
	if( buffer3d )
		buffer3d->Release(); 
	buffer3d = 0;

	buffer->Release(); 
	buffer = 0;
}

void ChannelStatic::play(DataSource * source)
{
	if( !get_rep() )
		return;
	stop();
	position = 0;
	finished = false;
	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		get_rep_name().hardware->play_static_3d_channel(this);
	}
	else
	{
		come_to_existance();
	}
}

void ChannelStatic::stop()
{
	if( !get_rep() )
		return;
	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		get_rep_name().hardware->stop_static_3d_channel(this);
	}
	if( in_existance() )
		begone();
	finished = true;
	position = 0;
}

bool ChannelStatic::is_finished()
{
	if( !get_rep() )
		return true;
	if( get_rep_name().channel_type != hard::Hardware::channel_type_3d )
		life_cycle(0, math::Vector3<float>() );
	return finished;
}

void ChannelStatic::on_hardware_delete()
{
	stop();
	unload_rep();
}

ChannelStreaming::ChannelStreaming(Hardware * hardware, const Atom & channel_type, double length_sec):
	hardware( hardware ),
	channel_type( channel_type )
{
//	logs::log() << "liba::noise::hard::directx8::Channel::Channel() I can perform well if fps is greater than " << min_good_fps << "\n";
	DSBUFFERDESC desc;
	memset( &desc, 0, sizeof(DSBUFFERDESC) );
    desc.dwSize = sizeof(DSBUFFERDESC); 

	memset( &wfx, 0, sizeof(WAVEFORMATEX) );

	if( channel_type == hard::Hardware::channel_type_2d )
	{
		wfx.Format.wFormatTag = WAVE_FORMAT_PCM;
		wfx.Format.nChannels = 2;
		wfx.Format.nSamplesPerSec = frequency_2d;
		wfx.Format.wBitsPerSample = 16;
		wfx.Format.nBlockAlign = wfx.Format.wBitsPerSample / 8 * wfx.Format.nChannels;
		wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign;

		desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 |   // Always a good idea
			DSBCAPS_GLOBALFOCUS |
			DSBCAPS_CTRLPAN |
			DSBCAPS_CTRLFREQUENCY |
			DSBCAPS_CTRLVOLUME;
	}
	if( channel_type == hard::Hardware::channel_type_3d )
	{
		wfx.Format.wFormatTag = WAVE_FORMAT_PCM;
		wfx.Format.nChannels = 1;
		wfx.Format.nSamplesPerSec = frequency_3d;
		wfx.Format.wBitsPerSample = 16;
		wfx.Format.nBlockAlign = wfx.Format.wBitsPerSample / 8 * wfx.Format.nChannels;
		wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign;

		desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 |   // Always a good idea
			DSBCAPS_GLOBALFOCUS |
			DSBCAPS_CTRL3D |
			DSBCAPS_CTRLVOLUME;
	}
	if( channel_type == hard::Hardware::channel_type_surround )
	{
		wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
		wfx.Format.nChannels = 6;
		wfx.Format.nSamplesPerSec = frequency_surround;
		wfx.Format.wBitsPerSample = 16;
		wfx.Format.cbSize = 22;
		wfx.Format.nBlockAlign = wfx.Format.nChannels * wfx.Format.wBitsPerSample / 8;
		wfx.Format.nAvgBytesPerSec = wfx.Format.nBlockAlign * wfx.Format.nSamplesPerSec;
		wfx.Samples.wValidBitsPerSample = 16;
		wfx.dwChannelMask =  SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT |
							SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY |
							SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT;
		wfx.SubFormat =  KSDATAFORMAT_SUBTYPE_PCM;  // Specify PCM

		desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 |   // Always a good idea
			DSBCAPS_GLOBALFOCUS |
			DSBCAPS_CTRLVOLUME;
	}

	desc.lpwfxFormat = &wfx.Format;
	size = desc.dwBufferBytes = wfx.Format.nBlockAlign * int(wfx.Format.nSamplesPerSec * length_sec);
	minimum_update_size = unsigned(size * minimum_update_part);
	HRESULT hr = hardware->get_device()->CreateSoundBuffer( &desc, &buffer, 0 );

	if( channel_type == hard::Hardware::channel_type_3d )
	{
		if( buffer )
			hr = buffer->QueryInterface( IID_IDirectSound3DBuffer, (void**)&buffer3d );
	}

	playing = false;
	hardware->add_streaming_channel( this );
}

ChannelStreaming::~ChannelStreaming()
{
	hardware->remove_streaming_channel( this );

	destroy_sources();
	if( buffer3d )
		buffer3d->Release();
	buffer3d = 0;
	if( buffer )
		buffer->Release();
	buffer = 0;
}

void ChannelStreaming::destroy_sources()
{
	while( sources.size() )
	{
		delete sources.back();
		sources.pop_back();
	}
}

void ChannelStreaming::play(DataSource * source)
{
	if( !source )
		return; // ok - added empty datasource
	sources.push_back( source );
	if( sources.size() == 1 ) // first buffer, sound can still be playing...
	{
		if( buffer )
		{
			buffer->Stop();
			buffer->SetCurrentPosition( 0 );
			garbage_position = 0;
			silence_size = 0;
			playing = true;
			update(0, size); // We fill the whole buffer :)
			buffer->Play( 0, 0, DSBPLAY_LOOPING );
			life_cycle(0);
		}
	}
}

void ChannelStreaming::stop()
{
	playing = false;
	if( buffer )
		buffer->Stop();
	destroy_sources();
}

bool ChannelStreaming::is_finished()
{
	return !playing;
}

void ChannelStreaming::update( DWORD byte_start, DWORD byte_count )
{
///	if( this == FocusChannel )
///	logs::log() << (int)this << " liba::noise::hard::directx8::Channel::update {" << (int)this << "} " << int(byte_start) << " - " << int(byte_count) << "\n";
	if( byte_count == 0 )
		return;
	void * ptr1, * ptr2;
	DWORD bytes1, bytes2;
	HRESULT hr = buffer->Lock( byte_start, byte_count,
				  &ptr1, &bytes1, &ptr2, &bytes2,
				  0 );
	if( FAILED( hr ) )
		return; // will try the next time :)

	int required_samples = bytes1 / wfx.Format.nBlockAlign;
	char * dst_ptr = reinterpret_cast<char *>( ptr1 );
	int s = 0;
	while( required_samples && (s = sources.size()) )
	{
		int decoded_samples = sources.front()->decode( required_samples );
		if( decoded_samples == 0 )
		{
			if( sources.size() > 1 || !loop)
			{
				sources.pop_front();
				continue;
			}
			else
			{
				// This is a hack that prevents MP3 to be looped.
				break;
			}
			continue;
		}
		required_samples -= decoded_samples;

		if( channel_type == hard::Hardware::channel_type_2d )
		{
			sources.front()->get_data( DataSource::STEREO_LEFT, dst_ptr, wfx.Format.nBlockAlign, 16 );
			sources.front()->get_data( DataSource::STEREO_RIGHT, dst_ptr + 2, wfx.Format.nBlockAlign, 16 );
		}
		if( channel_type == hard::Hardware::channel_type_3d )
		{
			sources.front()->get_data( DataSource::MONO, dst_ptr, wfx.Format.nBlockAlign, 16 );
		}
		if( channel_type == hard::Hardware::channel_type_surround )
		{
			sources.front()->get_data( DataSource::SURROUND_FRONTLEFT, dst_ptr, wfx.Format.nBlockAlign, 16 );
			sources.front()->get_data( DataSource::SURROUND_FRONTRIGHT, dst_ptr + 2, wfx.Format.nBlockAlign, 16 );
			sources.front()->get_data( DataSource::SURROUND_CENTER, dst_ptr + 4, wfx.Format.nBlockAlign, 16 );
			sources.front()->get_data( DataSource::SURROUND_SUBWOOFER, dst_ptr + 6, wfx.Format.nBlockAlign, 16 );
			sources.front()->get_data( DataSource::SURROUND_REARLEFT, dst_ptr + 8, wfx.Format.nBlockAlign, 16 );
			sources.front()->get_data( DataSource::SURROUND_REARRIGHT, dst_ptr + 10, wfx.Format.nBlockAlign, 16 );
		}
		dst_ptr += decoded_samples * wfx.Format.nBlockAlign;
		silence_size = 0;
	}
	if( required_samples )
	{
		silence_size += required_samples * wfx.Format.nBlockAlign;
		memset( dst_ptr, 0, required_samples * wfx.Format.nBlockAlign );
	}
	buffer->Unlock( ptr1, bytes1, ptr2, 0 ); // We tell driver that we have not written any data to second location
}

void ChannelStreaming::life_cycle(double delta_time)
{
	if( !buffer || !playing )//|| sources.empty() )
		return;
	DWORD play_cursor, write_cursor;
	buffer->GetCurrentPosition( &play_cursor, &write_cursor );

///	logs::log() << (int)this << " play_cursor=" << int(play_cursor) << " write_cursor=" << int(write_cursor) << " garbage_position=" << int(garbage_position) << " minimum_update_size= " << int(minimum_update_size) << "\n";

	if( play_cursor == write_cursor )
		return; // Hardware is slow and has not started our sound yet...

	if( write_cursor >= play_cursor ) // ---p--w------
	{
		if( garbage_position < write_cursor && garbage_position > play_cursor )
			garbage_position = write_cursor; // Bad situation correction
		if( garbage_position >= write_cursor )
		{
			if( size - garbage_position + play_cursor - garbage_position < minimum_update_size )
				return;
			update( garbage_position, size - garbage_position );
			garbage_position = 0;
			update( garbage_position, play_cursor - garbage_position );
			garbage_position = play_cursor;
		}
		if( garbage_position < play_cursor )
		{
			if( play_cursor - garbage_position < minimum_update_size )
				return;
			update( garbage_position, play_cursor - garbage_position );
			garbage_position = play_cursor;
		}
	}
	else // -w----------p-
	{
		if( garbage_position < write_cursor || garbage_position > play_cursor )
			garbage_position = write_cursor; // Bad situation correction

		if( play_cursor - garbage_position < minimum_update_size )
			return;
		update( garbage_position, play_cursor - garbage_position );
		garbage_position = play_cursor;
	}
	if( silence_size >= size )
	{
		playing = false;
		buffer->Stop();
	}
}

void ChannelStreaming::on_hardware_delete()
{
	if( buffer3d )
		buffer3d->Release();
	buffer3d = 0;
	if( buffer )
		buffer->Release();
	buffer = 0;
	hardware = 0;
	playing = false;
}

} // namespace directx8
} // namespace hard
} // namespace noise
} // namesapce liba

